package com.example.mq.server.diskdatamanager.file;

import com.example.mq.common.BinaryTool;
import com.example.mq.common.MqException;
import com.example.mq.server.messagemanager.Message;
import com.example.mq.server.virtualhost.MQueue;

import java.io.*;
import java.util.LinkedList;
import java.util.Scanner;

public class MessageFileManager {
    // 构造一个静态内部类, 来表示一个队列的 有效消息统计文件
    static public class Stat {
        public int totalCount;  // 总消息数量
        public int validCount;  // 有效消息数量
    }

    private String rl = "./diskdata/messages/";

    public void init() {
        // TODO
    }

    // 创建指定队列对应的文件和目录
    public void createQueueFiles(String queueName) throws IOException {

        // 1. 创建队列目录
        File baseDir = new File(getQueueDir(queueName));
        if (!baseDir.exists()) {
            boolean ok = baseDir.mkdirs(); // 创建目录
            if (!ok) {
                throw new IOException("创建目录失败! baseDir=" + baseDir.getAbsolutePath());
            }
        }

        // 2. 创建 消息文件
        File queueDataFile = new File(getQueueDataPath(queueName));
        if (!queueDataFile.exists()) {
            boolean ok = queueDataFile.createNewFile();
            if (!ok) {
                throw new IOException("创建文件失败! queueDataFile=" + queueDataFile.getAbsolutePath());
            }
        }

        // 3. 创建 有效消息统计文件
        File queueStatFile = new File(getQueueStatPath(queueName));
        if (!queueStatFile.exists()) {
            boolean ok = queueStatFile.createNewFile();
            if (!ok) {
                throw new IOException("创建文件失败! queueStatFile=" + queueStatFile.getAbsolutePath());
            }
        }

        // 4. 给有效消息统计文件 设定初始值 0\t0
        Stat stat = new Stat();
        stat.totalCount = 0;
        stat.validCount = 0;

        writeStat(queueName, stat);
    }

    private String getQueueDir(String queueName) {
        return rl + queueName;
    }

    // 获取 消息文件 路径
    private String getQueueDataPath(String queueName) {
        return getQueueDir(queueName) + "/queue_data.txt";
    }

    // 获取 有效消息统计文件 路径
    private String getQueueStatPath(String queueName) {
        return getQueueDir(queueName) + "/queue_stat.txt";
    }

    // 写入 有效消息统计文件
    private void writeStat(String queueName, Stat stat) {
        // 使用 PrintWrite 来写文件.
        // OutputStream 打开文件, 默认情况下, 会直接把原文件清空. 此时相当于新的数据覆盖了旧的.
        try (OutputStream outputStream = new FileOutputStream(getQueueStatPath(queueName))) {
            PrintWriter printWriter = new PrintWriter(outputStream);
            printWriter.write(stat.totalCount + "\t" + stat.validCount);
            printWriter.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 删除指定队列的目录和文件.
    public void destroyQueueFiles(String queueName) throws IOException {
        // 先删除文件，再删除目录
        File queueDataFile = new File(getQueueDataPath(queueName));
        boolean ok1 = queueDataFile.delete();
        File queueStatFile = new File(getQueueStatPath(queueName));
        boolean ok2 = queueStatFile.delete();
        File baseDir = new File(getQueueDir(queueName));
        boolean ok3 = baseDir.delete();
        if (!ok1 || !ok2 || !ok3) {
            throw new IOException("删除队列目录和文件失败! baseDir=" + baseDir.getAbsolutePath());
        }
    }

    // 存储指定消息到指定队列
    public void sendMessage(MQueue queue, Message message) throws MqException, IOException {
        // 1. 检查队列的目录及文件是否都存在
        if (!checkFilesExits(queue.getName())) {
            throw new MqException("[MessageFileManager] 队列对应的文件不存在! queueName=" + queue.getName());
        }
        // 2. 序列化 message 对象
        byte[] messageBinary = BinaryTool.toBytes(message);

        // 对队列对象进行加锁
        // 如果两个线程往同一个队列中写消息，此时需要阻塞等待；如果往不同队列中写消息，则不需要阻塞等待
        synchronized (queue) {

            // 3. 填充 offsetBeg 和 offsetEnd
            File queueDataFile = new File(getQueueDataPath(queue.getName()));
            message.setOffsetBeg(queueDataFile.length() + 4);// .length() 获取文件内容长度，单位字节
            message.setOffsetEnd(queueDataFile.length() + 4 + messageBinary.length);

            // 4. 写入 消息文件
            try (OutputStream outputStream = new FileOutputStream(queueDataFile, true)) {
                try (DataOutputStream dataOutputStream = new DataOutputStream(outputStream)) {
                    // 写入消息长度信息，.writeInt() 写四个 4 个字节
                    dataOutputStream.writeInt(messageBinary.length);
                    // 写入消息
                    dataOutputStream.write(messageBinary);
                }
            }
            // 5. 更新 有效消息统计文件
            Stat stat = readStat(queue.getName());
            stat.totalCount += 1;
            stat.validCount += 1;
            writeStat(queue.getName(), stat);
        }
    }

    // 检查队列的目录和文件是否存在
    public boolean checkFilesExits(String queueName) {
        File queueDataFile = new File(getQueueDataPath(queueName));
        if (!queueDataFile.exists()) {
            return false;
        }
        File queueStatFile = new File(getQueueStatPath(queueName));
        if (!queueStatFile.exists()) {
            return false;
        }
        return true;
    }

    // 读取 有效消息统计文件
    private Stat readStat(String queueName) {
        // 由于当前的消息统计文件是文本文件, 可以直接使用 Scanner 来读取文件内容
        Stat stat = new Stat();
        try (InputStream inputStream = new FileInputStream(getQueueStatPath(queueName))) {
            Scanner scanner = new Scanner(inputStream);
            stat.totalCount = scanner.nextInt();
            stat.validCount = scanner.nextInt();
            return stat;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    // 删除指定队列里的指定消息，逻辑删除
    public void deleteMessage(MQueue queue, Message message) throws IOException, ClassNotFoundException {
        synchronized (queue) {
            try (RandomAccessFile randomAccessFile = new RandomAccessFile(getQueueDataPath(queue.getName()), "rw")) {
                // 1. 从文件中读取对应的 Message 数据
                byte[] bufferSrc = new byte[(int) (message.getOffsetEnd() - message.getOffsetBeg())];
                randomAccessFile.seek(message.getOffsetBeg()); // .seek(long pos) 方法用于将文件指针移动到指定的位置
                randomAccessFile.read(bufferSrc);

                // 2. 反序列化，拿到 message 对象
                Message diskMessage = (Message) BinaryTool.fromBytes(bufferSrc);

                // 3. 将 isValid 设置为无效
                diskMessage.setIsValid((byte) 0x0);

                // 4. 写入文件
                byte[] bufferDest = BinaryTool.toBytes(diskMessage);
                // 重新调整文件光标
                randomAccessFile.seek(message.getOffsetBeg());
                randomAccessFile.write(bufferDest);
            }
            // 5. 更新统计文件
            Stat stat = readStat(queue.getName());
            if (stat.validCount > 0) {
                stat.validCount -= 1;
            }
            writeStat(queue.getName(), stat);
        }
    }

    // 读取指定队列里的所有消息
    // 由于该方法是在程序启动时调用，此时服务器还不能处理请求，因此不涉及多线程操作文件
    public LinkedList<Message> loadAllMessageFromQueue(String queueName) throws IOException, MqException, ClassNotFoundException {
        LinkedList<Message> messages = new LinkedList<>();
        try (InputStream inputStream = new FileInputStream(getQueueDataPath(queueName))) {
            try (DataInputStream dataInputStream = new DataInputStream(inputStream)) {

                // 光标
                long currentOffset = 0;

                // 一个文件中包含了多条消息，故 循环读取
                while (true) {
                    // 1. 读取当前消息的长度
                    // readInt 可能会读到文件的末尾(EOF)，并抛出 EOFException 异常
                    int messageSize = dataInputStream.readInt();

                    // 2. 按照这个长度, 读取消息内容
                    byte[] buffer = new byte[messageSize];
                    int actualSize = dataInputStream.read(buffer);
                    if (messageSize != actualSize) {
                        // 如果不匹配, 说明文件有问题, 格式错乱了
                        throw new MqException("[MessageFileManager] 文件格式错误! queueName=" + queueName);
                    }

                    // 3. 反序列化，拿到 message 对象
                    Message message = (Message) BinaryTool.fromBytes(buffer);

                    // 4. 判定消息是否有效
                    if (message.getIsValid() != 0x1) {
                        // 无效数据，仅更新光标即可
                        currentOffset += (4 + messageSize);
                        continue;
                    }

                    // 有效数据
                    // 因为 消息所在文件位置记录信息 并没有被序列化存储在文件中，故需要更新此记录信息
                    message.setOffsetBeg(currentOffset + 4);
                    message.setOffsetEnd(currentOffset + 4 + messageSize);
                    // 更新光标
                    currentOffset += (4 + messageSize);
                    // 添加进链表
                    messages.add(message);
                }
            } catch (EOFException e) {
                // 这个 catch 并非真是处理 "异常", 而是处理 "正常" 的业务逻辑. 文件读到末尾, 会被 readInt 抛出该异常.
                System.out.println("[MessageFileManager] 加载 Message 数据完成!");
            }
        }
        return messages;
    }

    // GC
    public void gc(MQueue queue) throws MqException, IOException, ClassNotFoundException {
        // 上锁
        // 进行 gc 的时候, 是针对消息数据文件进行大洗牌. 在这个过程中, 其他线程不能针对该队列的消息文件做任何修改
        synchronized (queue) {
            // 由于 gc 操作可能比较耗时, 此处统计一下执行消耗的时间.
            // 记录当前时刻
            long gcBeg = System.currentTimeMillis();

            // 1. 创建一个新的文件
            File queueDataNewFile = new File(getQueueDataNewPath(queue.getName()));
            if (queueDataNewFile.exists()) {
                // 正常情况下, 这个文件不应该存在. 如果存在, 就是意外~~ 说明上次 gc 了一半, 程序意外崩溃了.
                throw new MqException("[MessageFileManager] gc 时发现该队列的 queue_data_new 已经存在! queueName=" + queue.getName());
            }
            boolean ok = queueDataNewFile.createNewFile();
            if (!ok) {
                throw new MqException("[MessageFileManager] 创建文件失败! queueDataNewFile=" + queueDataNewFile.getAbsolutePath());
            }

            // 2. 从旧的文件中, 读取出所有的有效消息对象了. (这个逻辑直接调用上述方法即可, 不必重新写了)
            LinkedList<Message> messages = loadAllMessageFromQueue(queue.getName());

            // 3. 把有效消息, 写入到新的文件中.
            try (OutputStream outputStream = new FileOutputStream(queueDataNewFile)) {
                try (DataOutputStream dataOutputStream = new DataOutputStream(outputStream)) {
                    for (Message message : messages) {
                        // 写入
                        byte[] buffer = BinaryTool.toBytes(message);
                        dataOutputStream.writeInt(buffer.length);
                        dataOutputStream.write(buffer);
                    }
                }
            }

            // 4. 删除旧的数据文件, 并且把新的文件进行重命名
            File queueDataOldFile = new File(getQueueDataPath(queue.getName()));
            ok = queueDataOldFile.delete();
            if (!ok) {
                throw new MqException("[MessageFileManager] 删除旧的数据文件失败! queueDataOldFile=" + queueDataOldFile.getAbsolutePath());
            }
            // 把 queue_data_new.txt => queue_data.txt
            ok = queueDataNewFile.renameTo(queueDataOldFile);
            if (!ok) {
                throw new MqException("[MessageFileManager] 文件重命名失败! queueDataNewFile=" + queueDataNewFile.getAbsolutePath()
                        + ", queueDataOldFile=" + queueDataOldFile.getAbsolutePath());
            }

            // 5. 更新统计文件
            Stat stat = readStat(queue.getName());
            stat.totalCount = messages.size();
            stat.validCount = messages.size();
            writeStat(queue.getName(), stat);

            // 记录当前时刻
            long gcEnd = System.currentTimeMillis();

            System.out.println("[MessageFileManager] gc 执行完毕! queueName=" + queue.getName() + ", time="
                    + (gcEnd - gcBeg) + "ms");
        }
    }

    private String getQueueDataNewPath(String queueName) {
        return getQueueDir(queueName) + "/queue_data_new.txt";
    }

    // 检查当前是否要针对该队列的消息数据文件进行 GC
    public boolean checkGC(String queueName) {
        // 判定是否要 GC, 是根据总消息数和有效消息数. 这两个值都是在 消息统计文件 中的
        Stat stat = readStat(queueName);
        if (stat.totalCount > 2000 && (double)stat.validCount / (double)stat.totalCount < 0.5) {
            return true;
        }
        return false;
    }
}
